﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using NetworkDetectionUtilities;
using SharpDropBoxClient.db;
using SimpleJson;

#if !WINDOWS_PHONE && SILVERLIGHT
using System.Windows.Browser;
#endif

namespace SharpDropBoxClient
{
    public enum DropBoxAreas
    {
        dropbox,
        sandbox
    }

    public class DropboxNetworkNotAvailable : WebException
    {
        public DropboxNetworkNotAvailable()
        {
        }
    }

    public class DropboxNetworkNotAvailableAndNoCacheValueAvailable : Exception
    {
        public DropboxNetworkNotAvailableAndNoCacheValueAvailable()
        {
        }
    }

    public interface IDropBoxApp
    {
        bool IsConnected { get; }

        DropBoxAreas DropBoxArea { get; set; }

        bool NeedAuthInfo { get; }

        void LoginAndGetAuthCredentials(string userName, string password, Action<bool, Exception> completeCallback);

        void GetAccountInfo(Action<IDictionary<string, object>, Exception> callback);

        void CreateAccount(string email, string pwd, string firstName, string lastName, Action<string, Exception> callback);

        /// <summary>
        /// call /metadata
        /// The metadata API location provides the ability to retrieve
        /// file and folder metadata and manipulate the directory structure by moving or deleting files and folders.
        /// </summary>
        /// <param name="path"></param>
        /// <param name="callback"></param>
        /// <remarks></remarks>
        void GetFileEntry(string path, List<string> parms, System.Action<MetadataCallResult> callback);

        void ListDirAndContents(string path, List<string> parms, System.Action<MetadataCallResult> callback);

        IDictionary<string, object> GetLastDirInfoResults(string path);

        DBoxListing GetLastListingResults(string path);

        void GetFile(string path, Action<string, Exception> callback);

        void GetFileURI(string path, Action<string, Exception> callback);

        void GetThumbbnail(string path, Action<Stream, Exception> callback);

        void GetThumbbnail(string path, DropBoxApp.ThumbnailSize size, Action<Stream, Exception> callback);

        void GetThumbbnail(string path, DropBoxApp.ThumbnailFormat format, Action<Stream, Exception> callback);

        void GetThumbbnail(string path, DropBoxApp.ThumbnailSize size, DropBoxApp.ThumbnailFormat format, Action<Stream, Exception> callback);

        void GetThumbbnailURI(string path, Action<string, Exception> callback);

        void GetThumbbnailURI(string path, DropBoxApp.ThumbnailSize size, Action<string, Exception> callback);

        void GetThumbbnailURI(string path, DropBoxApp.ThumbnailFormat format, Action<string, Exception> callback);

        void GetThumbbnailURI(string path, DropBoxApp.ThumbnailSize size, DropBoxApp.ThumbnailFormat format, Action<string, Exception> callback);

        /// <summary>
        /// This is not currently working. We are using Hammock and they are working to resolve the issue.
        /// When they fix it this will work
        /// </summary>
        /// <param name="dirPath"></param>
        /// <param name="fileName"></param>
        /// <param name="file"></param>
        /// <param name="callback"></param>
        void UploadFile(string dirPath, string fileName, Stream file, Action<Exception> callback);

        void DeletePathOrFile(string path, Action<Exception> callback);

        void CopyPathOrFile(string srcPath, string destPath, Action<MetadataCallResult> callback);

        void MovePathOrFile(string srcPath, string destPath, Action<MetadataCallResult> callback);

        void CreateDirectory(string path, Action<MetadataCallResult> callback);

        bool MimicDropBoxFileSystemLocally { get; set; }
    }

    public class DropBoxApp : ThuziStyleRestClientApiWithSignedOAuthUsingAppTokensAndUserTokens, INotifyPropertyChanged, IDropBoxApp
    {
        public DropBoxApp(string apiKey, string apiSecret)
            : this(apiKey, apiSecret, new PolenterDbCache())
        {
        }

        public DropBoxApp(string apiKey, string apiSecret, IDbCache db)
            : this(apiKey, apiSecret, db, null) //, NetworkDetectorUtility.Instance)
        {
        }

        public DropBoxApp(string apiKey, string apiSecret, NetworkDetectorUtility netDetector)
            : this(apiKey, apiSecret, new PolenterDbCache(), netDetector)
        {
        }

        public DropBoxApp(string apiKey, string apiSecret, IDbCache db, INetworkDetector netDetector)
        {
            this.apiSecret = apiSecret;
            this.apiKey = apiKey;
            this.db = db;
            if (netDetector != null)
            {
                this.netDetector = netDetector;
                netDetector.OnNetworkChanged += new EventHandler<NetworkAvailableEventArgs>(netDetector_OnNetworkChanged);
            }
        }

        bool networkIsConnected = true;

        public bool IsConnected
        {
            get
            {
                return networkIsConnected;
            }
        }

        void netDetector_OnNetworkChanged(object sender, NetworkAvailableEventArgs e)
        {
            networkIsConnected = e.IsOnline;
            RaisePropertyChanged("IsConnected");
        }

        public DropBoxAreas DropBoxArea
        {
            get;
            set;
        }

        private string getDropboxSubDir()
        {
            return @"/" + DropBoxArea.ToString();
        }

        public bool NeedAuthInfo
        {
            get
            {
                return !(HaveUserSecret && HaveToken);
            }
        }

        public void LoginAndGetAuthCredentials(string userName, string password, Action<bool, Exception> completeCallback)
        {
            if (networkIsConnected)
            {
                DBXxAuth(APIBASEURL, TOKENCOMMAND, userName, password,
                             (result) =>
                             {
                                 if ((int)result.StatusCode == 400)
                                 {
                                     completeCallback(false, new DropBoxApiException("Need Mobile Permission", "APP PERMISSION"));
                                 }
                                 else if ((int)result.StatusCode == 401)
                                 {
                                     Token = "";
                                     UserSecret = "";
                                     completeCallback(false, new DropBoxApiException("Need To Re-Login;Stored Credentials have been cleared", "RE-AUTH"));
                                 }
                                 else if (result.Error != null)
                                 {
                                     completeCallback(false, result.Error);
                                 }
                                 else
                                 {
                                     var resultInfo = (IDictionary<string, object>)result.Result;
                                     if (resultInfo.ContainsKey("token"))
                                         Token = (string)resultInfo["token"];
                                     if (resultInfo.ContainsKey("secret"))
                                         UserSecret = (string)resultInfo["secret"];

                                     completeCallback(true, null);
                                 }
                             });
            }
            else
                throw new DropboxNetworkNotAvailable();
        }

        public void GetAccountInfo(Action<IDictionary<string, object>, Exception> callback)
        {
            // TODO: add Caching here so we can get a result offline
            if (networkIsConnected)
            {
                DoAsyncRetrieve(APIBASEURL + ACCOUNTINFOCOMMAND, "", new List<string>(), "GET", (result) =>
                {
                    if (result.Error != null)
                    {
                        callback(null, result.Error);
                    }
                    else
                    {
                        callback((IDictionary<string, object>)result.Result, null);
                    }
                });
            }
            else
                throw new DropboxNetworkNotAvailable();
        }

        // TODO: Do Account creation
        public void CreateAccount(string email, string pwd, string firstName, string lastName, Action<string, Exception> callback)
        {
            Token = "";
            userSecret = "";

            if (networkIsConnected)
            {
                DoAsyncRetrieve(APIBASEURL + ACCOUNTCOMMAND, "",
                    (new[] { "email=" + email, "first_name=" + firstName, "last_name=" + lastName, "password=" + pwd }).ToList(), "POST", (result) =>
                {
                    if (result.Error != null)
                    {
                        callback(null, result.Error);
                    }
                    else
                    {
                        callback((string)result.Content, null);
                    }
                });
            }
            else
                throw new DropboxNetworkNotAvailable();
        }

        /// <summary>
        /// call /metadata
        /// The metadata API location provides the ability to retrieve
        /// file and folder metadata and manipulate the directory structure by moving or deleting files and folders.
        /// </summary>
        /// <param name="path"></param>
        /// <param name="callback"></param>
        /// <remarks></remarks>

        public void GetFileEntry(string path, List<string> parms, System.Action<MetadataCallResult> callback)
        {
            var area = "";
            var dbPath = GetDbPathAndCleansePath(ref path, out area);

            var cachedValue = db.getMetaDataEntry(dbPath);

            if (cachedValue == null)
            {
                cachedValue = new MetaDataEntry { Path = path };
            }
            else if (!String.IsNullOrEmpty(cachedValue.Hash))
            {
                parms.Add("hash=" + cachedValue.Hash);
            }
            if (networkIsConnected)
            {
                DoAsyncRetrieve(APIBASEURL + LISTCOMMAND + area, path, parms, "GET", (result) =>
                {
                    if ((int)result.StatusCode == 304 || (String.IsNullOrEmpty(result.Content) && cachedValue != null)) // No Changes
                    {
                        useCachedVersion(path, callback, cachedValue);
                    }

                    else if ((int)result.StatusCode == 400)
                    {
                        callback(new MetadataCallResult(path, false, null, null, new DropBoxApiException("This app does not have the necessary permissions to do this.", "APP PERMISSIONS")));
                    }
                    else if ((int)result.StatusCode == 404)
                    {
                        callback(new MetadataCallResult(path, false, null, null, new FileNotFoundException()));
                    }
                    else if ((int)result.StatusCode == 406)
                    {
                        callback(new MetadataCallResult(path, false, null, null, new System.IO.IOException("Too many files to return")));
                    }
                    else if (result.Error != null)
                    {
                        callback(new MetadataCallResult(path, false, null, null, result.Error));
                    }
                    else
                    {
                        var json = JsonSerializer.SerializeObject((JsonObject)result.Result);
                        // there were changes somewhere.. Let's detect deleted files and remove them from IsoStorage
                        DBoxListing listingNew = null;
                        try
                        {
                            listingNew = DBoxListing.FromDictionary((IDictionary<string, object>)JsonSerializer.DeserializeObject(json));
                        }
                        catch (Exception ex)
                        { }
                        if (!String.IsNullOrEmpty(cachedValue.JSON))
                        {
                            CleanupCachedFiles(listingNew, area, cachedValue);
                        }
                        db.SaveMetaDataEntry(new MetaDataEntry { Path = dbPath, JSON = json, Hash = listingNew.Hash ?? "" });

                        callback(new MetadataCallResult(path, false, (IDictionary<string, object>)result.Result, listingNew, null));
                    }
                });
            }
            else if (cachedValue != null)
                useCachedVersion(path, callback, cachedValue);
            else
                throw new DropboxNetworkNotAvailableAndNoCacheValueAvailable();
        }

        public void ListDirAndContents(string path, List<string> parms, System.Action<MetadataCallResult> callback)
        {
            foreach (var parm in parms)
            {
                if (parm.StartsWith("list="))
                    parms.Remove(parm);
            }
            parms.Add("list=true");
            GetFileEntry(path, parms, callback);
        }

        private void CleanupCachedFiles(DBoxListing listingNew, string area, MetaDataEntry cachedValue)
        {
            DBoxListing listingOld = null;
            try
            {
                listingOld = DBoxListing.FromDictionary((IDictionary<string, object>)JsonSerializer.DeserializeObject(cachedValue.JSON));
            }
            catch (Exception ex) { }
            if (listingOld == null || listingOld.Contents == null)
                return;
            foreach (DBoxListing item in listingOld.Contents)
            {
                if (!item.IsDirectory)
                {
                    if (listingNew != null && listingNew.Contents != null && listingNew.Contents.Where(di => (di.Name ?? "").Equals(item.Name, StringComparison.InvariantCultureIgnoreCase)).Count() == 0)
                    {
                        // need to delete this file
                        var filepath = cleanPath(item.FullPath);

                        var filedbPath = db.getDbPathFromAreaAndPath(area, filepath);
                        if (PSFile.Exists(filedbPath))
                        {
                            try
                            {
                                PSFile.Delete(filedbPath);
                            }
                            catch (Exception ex) { 
                                /* ignoring exception */
                                Debug.WriteLine("In {1}.{2} tagged {3}, swallowing Error {0}", ex, "DropBoxApp", "CleanupCachedFiles()", "deletePathOrFile");
                            }
                        }
                    }
                }
                else
                {
                    if (listingNew != null && listingNew.Contents != null && listingNew.Contents.Where(di => di.Name.Equals(item.Name, StringComparison.InvariantCultureIgnoreCase)).Count() == 0)
                    {
                        // need to delete this file
                        var dirpath = cleanPath(item.FullPath);

                        var dirdbPath = db.getDbPathFromAreaAndPath(area, dirpath);
                        if (PSFile.Exists(dirdbPath))
                        {
                            DeleteLocalFolder(new PSDirectoryInfo(dirdbPath));
                        }
                    }
                }
            }
        }

        private void DeleteLocalFolder(PSDirectoryInfo dir)
        {
            if (dir.Exists)
            {
                foreach (var file in dir.GetFiles())
                {
                    try
                    {
                        file.Delete();
                    }
                    catch (Exception ex) {
                        Debug.WriteLine("In {1}.{2} tagged {3}, swallowing Error {0}", ex, "DropBoxApp", "DeleteLocalFolder()", "filedelete");
                    }
                }
                foreach (var subDir in dir.GetDirectories())
                {
                    DeleteLocalFolder(dir);
                }
                foreach (var file in dir.GetFiles())
                {
                    try
                    {
                        dir.Delete();
                    }
                    catch (Exception ex) 
                    {
                        Debug.WriteLine("In {1}.{2} tagged {3}, swallowing Error {0}", ex, "DropBoxApp", "DeleteLocalFolder()", "dirdelete");
                    }
                }
            }
        }

        public IDictionary<string, object> GetLastDirInfoResults(string path)
        {
            var area = "";
            string dbPath = GetDbPathAndCleansePath(ref path, out area);

            var cachedValue = db.getMetaDataEntry(dbPath);
            if (cachedValue == null)
            {
                cachedValue = new MetaDataEntry { Path = path };
            }
            try
            {
                return (IDictionary<string, object>)JsonSerializer.DeserializeObject(cachedValue.JSON);
            }
            catch (Exception ex)
            {
                return null;
            }
        }

        public DBoxListing GetLastListingResults(string path)
        {
            return DBoxListing.FromDictionary(GetLastDirInfoResults(path));
        }

        private static void useCachedVersion(string path, System.Action<MetadataCallResult> callback, MetaDataEntry cachedValue)
        {
            IDictionary<string, object> cachedJson = null;
            try
            {
                cachedJson = (IDictionary<string, object>)JsonSerializer.DeserializeObject(cachedValue.JSON);
            }
            catch (Exception ex) { }
            callback(new MetadataCallResult(path, true, cachedJson, DBoxListing.FromDictionary(cachedJson), null));
        }

        public void GetFile(string path, Action<string, Exception> callback)
        {
            var area = getDropboxSubDir();
            path = cleanPath(path);
            var dbPath = db.getDbPathFromAreaAndPath(area, path);
            string isoStorePath = GetIsoStorePathFromDbPath(dbPath);
            GetFileEntry(path, new List<string>(), (dirInfo) =>
            {
                if (dirInfo.Cached && PSFile.Exists(isoStorePath))
                {
                    callback(isoStorePath, null);
                }
                else
                {
                    if (networkIsConnected)
                    {
                        DoAsyncRetrieveToFile(APISUBDOMAINBASEURL + FILESCOMMAND, area + (path.StartsWith("/") || area.EndsWith("/") ? "" : "/") + path, new List<string>(), "GET", isoStorePath, (result) =>
                        {
                            if (result.Error != null)
                            {
                                callback("", result.Error);
                            }
                            else
                            {
                                callback((string)result.Result, null);
                            }
                        });
                    }
                    else
                        throw new DropboxNetworkNotAvailableAndNoCacheValueAvailable();
                }
            });
        }

        private string GetIsoStorePathFromDbPath(string dbPath)
        {
            return dbPath;
        }

        public void GetFileURI(string path, Action<string, Exception> callback)
        {
            var area = getDropboxSubDir();
            path = cleanPath(path);
            DoAsyncRetrieveURI(APISUBDOMAINBASEURL + FILESCOMMAND, area + (path.StartsWith("/") || area.EndsWith("/") ? "" : "/") + path, new List<string>(), "GET", (result) =>
            {
                if (result.Error != null)
                {
                    callback("", result.Error);
                }
                else
                {
                    callback((string)result.Result, null);
                }
            });
        }

        public String GetFileURI(string path)
        {
            var area = getDropboxSubDir();
            path = cleanPath(path);
            var result = DoRetrieveURI(APISUBDOMAINBASEURL + FILESCOMMAND, area + (path.StartsWith("/") || area.EndsWith("/") ? "" : "/") + path, new List<string>(), "GET");
            if (result.Error != null)
            {
                Debug.WriteLine("GetFileURI Exception: {0}", result.Error.Message);
                return "";
            }
            else
            {
                return result.Result.ToString();
            }
        }

        public enum ThumbnailSize
        {
            small, medium, large
        }
        public enum ThumbnailFormat
        {
            JPEG, PNG
        }

        public void GetThumbbnail(string path, Action<Stream, Exception> callback)
        {
            GetThumbbnail(path, ThumbnailSize.small, ThumbnailFormat.JPEG, callback);
        }

        public void GetThumbbnail(string path, ThumbnailSize size, Action<Stream, Exception> callback)
        {
            GetThumbbnail(path, size, ThumbnailFormat.JPEG, callback);
        }

        public void GetThumbbnail(string path, ThumbnailFormat format, Action<Stream, Exception> callback)
        {
            GetThumbbnail(path, ThumbnailSize.small, format, callback);
        }

        public void GetThumbbnail(string path, ThumbnailSize size, ThumbnailFormat format, Action<Stream, Exception> callback)
        {
            var area = getDropboxSubDir();
            path = cleanPath(path);
            var dbPath = db.getDbPathFromAreaAndPath(area, path);

            // TODO: Add ISO cache of TNs
            if (networkIsConnected)
            {
                // TODO: base this on a file not a stream
                DoAsyncRetrieveStream(APISUBDOMAINBASEURL + THUMBNAILSCOMMAND, area + (path.StartsWith("/") || area.EndsWith("/") ? "" : "/") + path, (new[] { "size=" + size.ToString(), "format=" + format.ToString() }).ToList(), "GET", (result) =>
                {
                    if (result.Error != null)
                    {
                        callback(null, result.Error);
                    }
                    else if ((int)result.StatusCode == 404)
                    {
                        callback(null, new Exception("Can not create thumbnail for file"));
                    }
                    else if ((int)result.StatusCode == 415)
                    {
                        callback(null, new Exception("Source Image is Invalid"));
                    }
                    else if ((int)result.StatusCode == 500)
                    {
                        callback(null, new Exception("System Error"));
                    }
                    else
                    {
                        callback((Stream)result.Result, null);
                    }
                });
            }
            else
                throw new DropboxNetworkNotAvailableAndNoCacheValueAvailable();
        }

        public void GetThumbbnailURI(string path, Action<string, Exception> callback)
        {
            GetThumbbnailURI(path, ThumbnailSize.small, ThumbnailFormat.JPEG, callback);
        }

        public void GetThumbbnailURI(string path, ThumbnailSize size, Action<string, Exception> callback)
        {
            GetThumbbnailURI(path, size, ThumbnailFormat.JPEG, callback);
        }

        public void GetThumbbnailURI(string path, ThumbnailFormat format, Action<string, Exception> callback)
        {
            GetThumbbnailURI(path, ThumbnailSize.small, format, callback);
        }

        public void GetThumbbnailURI(string path, ThumbnailSize size, ThumbnailFormat format, Action<string, Exception> callback)
        {
            var area = getDropboxSubDir();
            path = cleanPath(path);
            DoAsyncRetrieveURI(APISUBDOMAINBASEURL + THUMBNAILSCOMMAND, area + (path.StartsWith("/") || area.EndsWith("/") ? "" : "/") + path, (new[] { "size=" + size.ToString(), "format=" + format.ToString() }).ToList(), "GET", (result) =>
            {
                if (result.Error != null)
                {
                    callback("", result.Error);
                }
                else
                {
                    callback((string)result.Result, null);
                }
            });
        }

        /// <summary>
        /// This is not currently working. We are using Hammock and they are working to resolve the issue.
        /// When they fix it this will work
        /// </summary>
        /// <param name="dirPath"></param>
        /// <param name="fileName"></param>
        /// <param name="file"></param>
        /// <param name="callback"></param>
        public void UploadFile(string dirPath, string fileName, Stream file, Action<Exception> callback)
        {
            var area = getDropboxSubDir();
            dirPath = cleanPath(dirPath);
            var dbPath = db.getDbPathFromAreaAndPath(area, dirPath);

            if (networkIsConnected)
            {
                PostFile(APISUBDOMAINBASEURL + FILESCOMMAND, dbPath, file, fileName, (result) =>
                {
                    if (result.Error != null)
                    {
                        callback(result.Error);
                    }
                    else if ((int)result.StatusCode == 400)
                    {
                        callback(new InvalidOperationException("Not allowed"));
                    }
                    else if ((int)result.StatusCode == 404)
                    {
                        callback(new System.IO.DirectoryNotFoundException());
                    }
                    else if ((int)result.StatusCode == 411)
                    {
                        callback(new System.IO.IOException("Chunked Encoding Not Supported"));
                    }
                    else
                    {
                        callback(null);
                    }
                });
            }
            else
                throw new DropboxNetworkNotAvailable();
        }

        public void DeletePathOrFile(string path, Action<Exception> callback)
        {
            var area = getDropboxSubDir().Replace("/", "");
            path = cleanPath(path);
            var dbPath = db.getDbPathFromAreaAndPath(area, path);
            if (networkIsConnected)
            {
                DoAsyncRetrieve(APIBASEURL + DELETECOMMAND, "", new List<string>(new[] { "path=" + path, "root=" + area }), "POST", (result) =>
                {
                    if ((int)result.StatusCode == 400)
                    {
                        callback(new FileNotFoundException("Root Path Not Found"));
                    }
                    else if ((int)result.StatusCode == 404)
                    {
                        callback(new FileNotFoundException());
                    }

                    if (result.Error != null)
                    {
                        callback(result.Error);
                    }
                    else
                    {
                        try
                        {
                            var entry = db.getMetaDataEntry(dbPath);
                            if (entry != null)
                            {
                                db.DeleteMetaDataEntry(entry);
                            }
                        }
                        catch (Exception ex) 
                        {
                            Debug.WriteLine("In {1}.{2} tagged {3}, swallowing Error {0}", ex, "DropBoxApp", "DeletePathOrFile()", "deletemetadata");
                        }
                        callback(null);
                    }
                });
            }
            else
                throw new DropboxNetworkNotAvailable();
        }

        public void CopyPathOrFile(string srcPath, string destPath, Action<MetadataCallResult> callback)
        {
            var area = getDropboxSubDir().Replace("/", "");
            srcPath = cleanPath(srcPath);
            destPath = cleanPath(destPath);
            var dbsrcPath = db.getDbPathFromAreaAndPath(area, srcPath);
            var dbPath = db.getDbPathFromAreaAndPath(area, destPath);
            if (networkIsConnected)
            {
                DoAsyncRetrieve(APIBASEURL + COPYCOMMAND, "", new List<string>(new[] { "from_path=" + srcPath, "to_path=" + destPath, "root=" + area }), "POST", (result) =>
                {
                    if ((int)result.StatusCode == 400)
                    {
                        callback(new MetadataCallResult(destPath, false, null, null, new FileNotFoundException("Root Path Not Found")));
                    }
                    else if ((int)result.StatusCode == 404)
                    {
                        callback(new MetadataCallResult(destPath, false, null, null, new FileNotFoundException()));
                    }

                    if (result.Error != null)
                    {
                        callback(new MetadataCallResult(destPath, false, null, null, result.Error));
                    }
                    else
                    {
                        var json = JsonSerializer.SerializeObject((JsonObject)result.Result);
                        // there were changes somewhere.. Let's detect deleted files and remove them from IsoStorage
                        DBoxListing listingNew = null;
                        try
                        {
                            listingNew = DBoxListing.FromDictionary((IDictionary<string, object>)JsonSerializer.DeserializeObject(json));
                        }
                        catch (Exception ex) { }
                        var srcEntry = db.getMetaDataEntry(dbsrcPath);
                        if (srcEntry != null)
                        {
                            try
                            {
                                var dirdbSrcPath = (area + cleanPath(srcEntry.Path)).Replace(@"//", @"/");
                                var dbDestPath = (area + cleanPath(listingNew.FullPath)).Replace(@"//", @"/");
                                if (!listingNew.IsDirectory)
                                {
                                    PSFile.Copy(dirdbSrcPath, dbDestPath);
                                }
                                else
                                {
                                    //PSDirectory.Copy(dirdbSrcPath, dbDestPath);
                                }
                            }
                            catch (Exception ex)
                            {
                                Debug.WriteLine("In {1}.{2} tagged {3}, swallowing Error {0}", ex, "DropBoxApp", "CopyPathOrFile()", "copy");
                            }
                        }
                        db.SaveMetaDataEntry(new MetaDataEntry { Path = dbPath, JSON = json, Hash = listingNew.Hash ?? "" });

                        callback(new MetadataCallResult(destPath, false, (IDictionary<string, object>)result.Result, listingNew, null));
                    }
                });
            }
            else
                throw new DropboxNetworkNotAvailable();
        }

        public void MovePathOrFile(string srcPath, string destPath, Action<MetadataCallResult> callback)
        {
            var area = getDropboxSubDir().Replace("/", "");
            srcPath = cleanPath(srcPath);
            destPath = cleanPath(destPath);
            var dbsrcPath = db.getDbPathFromAreaAndPath(area, srcPath);
            var dbPath = db.getDbPathFromAreaAndPath(area, destPath);
            if (networkIsConnected)
            {
                DoAsyncRetrieve(APIBASEURL + MOVECOMMAND, "", new List<string>(new[] { "from_path=" + srcPath, "to_path=" + destPath, "root=" + area }), "POST", (result) =>
                {
                    if ((int)result.StatusCode == 400)
                    {
                        callback(new MetadataCallResult(destPath, false, null, null, new FileNotFoundException("Root Path Not Found")));
                    }
                    else if ((int)result.StatusCode == 404)
                    {
                        callback(new MetadataCallResult(destPath, false, null, null, new FileNotFoundException()));
                    }

                    if (result.Error != null)
                    {
                        callback(new MetadataCallResult(destPath, false, null, null, result.Error));
                    }
                    else
                    {
                        var json = JsonSerializer.SerializeObject((JsonObject)result.Result);
                        // there were changes somewhere.. Let's detect deleted files and remove them from IsoStorage
                        DBoxListing listingNew = null;
                        try
                        {
                            listingNew = DBoxListing.FromDictionary((IDictionary<string, object>)JsonSerializer.DeserializeObject(json));
                        }
                        catch (Exception ex) { }

                        // We should probably move the file around in IsoStorage

                        var srcEntry = db.getMetaDataEntry(dbsrcPath);
                        if (srcEntry != null) // TODO detect folder and do something else
                        {
                            try
                            {
                                var dirdbSrcPath = (area + cleanPath(srcEntry.Path)).Replace(@"//", @"/");
                                var dbDestPath = (area + cleanPath(listingNew.FullPath)).Replace(@"//", @"/");
                                if (!listingNew.IsDirectory)
                                {
                                    PSFile.Move(dirdbSrcPath, dbDestPath);
                                }
                                else
                                {
                                    PSDirectory.Move(dirdbSrcPath, dbDestPath);
                                }
                            }
                            catch (Exception ex)
                            {
                                Debug.WriteLine("In {1}.{2} tagged {3}, swallowing Error {0}", ex, "DropBoxApp", "MovePathOrFile()", "move");
                            }

                            db.DeleteMetaDataEntry(srcEntry);
                            db.SaveMetaDataEntry(new MetaDataEntry { Path = dbPath, JSON = json, Hash = listingNew.Hash ?? "" });
                        }

                        callback(new MetadataCallResult(destPath, false, (IDictionary<string, object>)result.Result, listingNew, null));
                    }
                });
            }
            else
                throw new DropboxNetworkNotAvailable();
        }

        public void CreateDirectory(string path, Action<MetadataCallResult> callback)
        {
            var area = getDropboxSubDir().Replace("/", "");
            path = cleanPath(path);
            var dbPath = db.getDbPathFromAreaAndPath(area, path);

            if (networkIsConnected)
            {
                DoAsyncRetrieve(APIBASEURL + MAKEDIRCOMMAND, "", new List<string>(new[] { "path=" + path, "root=" + area }), "POST", (result) =>
                {
                    if ((int)result.StatusCode == 400)
                    {
                        callback(new MetadataCallResult(path, false, null, null, new FileNotFoundException("Root Path Not Found")));
                    }
                    else if ((int)result.StatusCode == 404)
                    {
                        callback(new MetadataCallResult(path, false, null, null, new FileNotFoundException()));
                    }

                    if (result.Error != null)
                    {
                        callback(new MetadataCallResult(path, false, null, null, result.Error));
                    }
                    else
                    {
                        var json = JsonSerializer.SerializeObject((JsonObject)result.Result);
                        // there were changes somewhere.. Let's detect deleted files and remove them from IsoStorage
                        DBoxListing listingNew = null;
                        try
                        {
                            DBoxListing.FromDictionary((IDictionary<string, object>)JsonSerializer.DeserializeObject(json));
                        }
                        catch (Exception ex) { }
                        try
                        {
                            var dbDestPath = (area + cleanPath(listingNew.FullPath)).Replace(@"//", @"/");

                            PSDirectory.CreateDirectory(dbDestPath);
                        }
                        catch (Exception ex)
                        {
                            Debug.WriteLine("In {1}.{2} tagged {3}, swallowing Error {0}", ex, "DropBoxApp", "CreateDirectory()", "CreateDirectory");
                        }

                        db.SaveMetaDataEntry(new MetaDataEntry { Path = dbPath, JSON = json, Hash = listingNew.Hash ?? "" });

                        callback(new MetadataCallResult(path, false, (IDictionary<string, object>)result.Result, listingNew, null));
                    }
                });
            }
            else
                throw new DropboxNetworkNotAvailable();
        }

        #region utility functions

        protected string cleanPath(string path)
        {
            path = (path ?? "").Replace(@"\", @"/");
            path = (path.StartsWith(@"/") ? path : @"/" + path);
            var result = new StringBuilder();
            var pathArray = path.Split(new[] { '/', '\\' });
            foreach (var item in pathArray)
            {
                if (!String.IsNullOrEmpty(item))
                {
                    result.Append("/" + HttpUtility.UrlEncode(item));
                }
            }
            //var sb = new StringBuilder();
            //foreach (var c in result.ToString())
            //{
            //    int tmp = c;
            //    if (c == '+')
            //    {
            //        sb.Append("%20");
            //    }
            //    else
            //        if (!(tmp >= 48 && tmp <= 122) && c != '.' && c != '/' && c != '-')
            //        {
            //            sb.AppendFormat("%{0:x2}", (uint)System.Convert.ToUInt32(tmp.ToString()));
            //        }
            //        else
            //            sb.Append(c);
            //}
            ////%=25
            //  ?   <   >
            //  %3C %3E
            var output = result.ToString().Replace("+", "%20").Replace("!", "%21")
                .Replace("^", "%5E").Replace("$", "%24").Replace("#", "%23")
                .Replace("'", "%27").Replace("(", "%28").Replace(")", "%29")
                .Replace("<", "%3C")
                .Replace(">", "%3E")
                .Replace("?", "%3F")
                .Replace(":", "%3A")
                .Replace(";", "%3B")
                .Replace("\"", "%22")
                .Replace("{", "%7B")
                .Replace("}", "%7D")
                .Replace("[", "%5B")
                .Replace("]", "%5D")
                .Replace("|", "%7C")
                .Replace("&", "%26")
                .Replace("~", "%7E")
                .Replace("`", "%60").Replace(",", "%2C").Replace("%2c", "%2C");
            //var output = sb.ToString();
            return (output.StartsWith("/") ? output.Substring(1) : output);
        }

        private string GetDbPathAndCleansePath(ref string path, out string area)
        {
            var pathEndedWithSlash = path.EndsWith(@"/") || path.EndsWith(@"\");
            path = HttpUtility.UrlDecode(path);
            area = getDropboxSubDir();
            path = cleanPath(path);
            var dbPath = db.getDbPathFromAreaAndPath(area, path);

            dbPath += (pathEndedWithSlash && !dbPath.EndsWith(@"/")) ? "/" : "";
            return dbPath;
        }

        #endregion utility functions

        #region INotifyPropertyChanged Members

        // Borrows very heavily from Laurent Bugnion 's MVVM Light
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises the PropertyChanged event if needed, and broadcasts a
        /// PropertyChangedMessage using the Messenger instance (or the
        /// static default instance if no Messenger instance is available).
        /// </summary>
        /// <typeparam name="T">The type of the property that
        /// changed.</typeparam>
        /// <param name="propertyName">The name of the property that
        /// changed.</param>
        /// <param name="oldValue">The property's value before the change
        /// occurred.</param>
        /// <param name="newValue">The property's value after the change
        /// occurred.</param>
        /// <param name="broadcast">If true, a PropertyChangedMessage will
        /// be broadcasted. If false, only the event will be raised.</param>
        [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate",
            Justification = "This cannot be an event")]
        protected virtual void RaisePropertyChanged<T>(string propertyName, T oldValue, T newValue, bool broadcast)
        {
            RaisePropertyChanged(propertyName);
        }

        /// <summary>
        /// Raises the PropertyChanged event if needed.
        /// </summary>
        /// <param name="propertyName">The name of the property that
        /// changed.</param>
        [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate",
            Justification = "This cannot be an event")]
        protected virtual void RaisePropertyChanged(string propertyName)
        {
            VerifyPropertyName(propertyName);

            var handler = PropertyChanged;

            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Verifies that a property name exists in this ViewModel. This method
        /// can be called before the property is used, for instance before
        /// calling RaisePropertyChanged. It avoids errors when a property name
        /// is changed but some places are missed.
        /// <para>This method is only active in DEBUG mode.</para>
        /// </summary>
        /// <param name="propertyName"></param>
        [Conditional("DEBUG")]
        [DebuggerStepThrough]
        public void VerifyPropertyName(string propertyName)
        {
            var myType = this.GetType();
            if (myType.GetProperty(propertyName) == null)
            {
                throw new ArgumentException("Property not found", propertyName);
            }
        }

        #endregion INotifyPropertyChanged Members
    }
}